package com.jagochan.imchat.module.websocket.service.impl;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.jagochan.imchat.module.user.dao.UserDao;
import com.jagochan.imchat.module.user.domain.entity.User;
import com.jagochan.imchat.module.user.service.LoginService;
import com.jagochan.imchat.module.websocket.adaptor.WebSocketAdaptor;
import com.jagochan.imchat.module.websocket.domain.dto.WSChannelExtraDTO;
import com.jagochan.imchat.module.websocket.domain.vo.resp.WSBaseResp;
import com.jagochan.imchat.module.websocket.service.WebSocketService;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class WebSocketServiceImpl implements WebSocketService {

    /**
     * 存储已经和ws建立连接的用户信息 <br>
     * 假如仅仅是和ws建立了连接，但是当前用户没有授权登陆，则VALUE中的uid为null
     */
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_CHANNEL_USER_MAP = new ConcurrentHashMap<>();

    private static final Long MAX_SIZE = 500L;
    private static final Duration EXPIRE_HOURS = Duration.ofHours(1);
    /**
     * 存储场景值code（我们自己生成的）和Channel的映射关系
     */
    private static final Cache<Integer, Channel> WAIT_LOGIN_CODE_CHANNEL_MAP = Caffeine.newBuilder()
            .maximumSize(MAX_SIZE)
            .expireAfterWrite(EXPIRE_HOURS)
            .build();

    @Lazy
    @Resource
    private WxMpService wxMpService;

    @Autowired
    private UserDao userDao;

    @Resource
    private LoginService loginService;

    @Override
    public void connect(Channel channel) {
        ONLINE_CHANNEL_USER_MAP.put(channel, new WSChannelExtraDTO());
    }

    /**
     * 返回微信登陆二维码的URL
     *
     * @param channel ws通道
     */
    @Override
    public void requestLoginQrCodeUrl(Channel channel) throws WxErrorException {
        // 存储code（场景值）和channel的映射关系
        Integer code = cacheCodeAndChannel(channel);
        // 向微信请求登陆链接
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, ((int) EXPIRE_HOURS.getSeconds()));

        // ws向客户端返回登陆链接
        wsSendMsg(channel, WebSocketAdaptor.buildLoginUrlResp(wxMpQrCodeTicket));
    }

    /**
     * 清除在线用户和Channel的缓存
     *
     * @param channel 通道
     */
    @Override
    public void remove(Channel channel) {
        ONLINE_CHANNEL_USER_MAP.remove(channel);
    }

    /**
     * 用户扫码登陆二维码后
     *
     * @param code 场景值（根据它可以获取到当前的Channel）
     */
    @Override
    public void waitAuthorize(Integer code) {
        Channel channel = WAIT_LOGIN_CODE_CHANNEL_MAP.getIfPresent(code);
        if (Objects.isNull(channel)) return;
        wsSendMsg(channel, WebSocketAdaptor.buildWaitAuthorize());
    }

    @Override
    public void scanLoginSuccess(Integer code, Long uid) {
        Channel channel = WAIT_LOGIN_CODE_CHANNEL_MAP.getIfPresent(code);
        if (channel == null) return;

        // 移除code和channel的缓存
        WAIT_LOGIN_CODE_CHANNEL_MAP.invalidate(code);

        String token = loginService.loginReturnToken(uid);

        User user = userDao.getById(uid);
        loginSuccess(channel, user, token);
    }

    /**
     * 客户端通过token认证
     *
     * @param channel
     * @param token   登陆令牌
     */
    @Override
    public void authorize(Channel channel, String token) {
        Long uid = loginService.getValidUid(token);
        if (Objects.isNull(uid)) {
            wsSendMsg(channel, WebSocketAdaptor.buildInvalidToken());
        } else {
            User user = userDao.getById(uid);
            loginSuccess(channel, user, token);
        }
    }

    private void loginSuccess(Channel channel, User user, String token) {
        // 修改当前登陆用户的uid（让channel和uid关联）
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_CHANNEL_USER_MAP.get(channel);
        wsChannelExtraDTO.setUid(user.getId());

        // TODO 发送【用户上线成功】的事件

        // ws向客户端推送消息
        wsSendMsg(channel, WebSocketAdaptor.buildAuthorizeSuccess(user, token));
    }

    private Integer cacheCodeAndChannel(Channel channel) {
        // 当指定的键（key）不存在于映射中，或者键对应的值为 null 时，才将指定的键值对插入到映射中；
        // 如果键已经存在，则不会修改现有值，并返回当前已存在的值。
        Integer code;
        do {
            code = RandomUtil.randomInt(1, Integer.MAX_VALUE);
        } while (Objects.nonNull(WAIT_LOGIN_CODE_CHANNEL_MAP.asMap().putIfAbsent(code, channel)));
        return code;
    }

    /**
     * ws向客户端发送数据
     *
     * @param channel 用于向客户端发送数据的通道
     * @param resp    要发送给客户端的数据
     */
    private void wsSendMsg(Channel channel, WSBaseResp<?> resp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(resp)));
    }
}
